////////////////////////////////////////////////////////////////////////////////
//
//  Copyright (c) 2010 Gabriel Montagné Láscaris-Comneno and Alberto
//  Brealey-Guzmán.
//
//  Permission is hereby granted, free of charge, to any person obtaining a
//  copy of this software and associated documentation files (the "Software"),
//  to deal in the Software without restriction, including without limitation
//  the rights to use, copy, modify, merge, publish, distribute, sublicense,
//  and/or sell copies of the Software, and to permit persons to whom the
//  Software is furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
//  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
//  DEALINGS IN THE SOFTWARE.
//
////////////////////////////////////////////////////////////////////////////////

package com.rojored.controls
{

import com.rojored.net.BitmapAssetDownloader;
import com.rojored.utils.MathUtil;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.events.Event;
import flash.events.IEventDispatcher;
import flash.geom.Matrix;
import flash.geom.Rectangle;
import flash.utils.getQualifiedClassName;
import mx.core.UIComponent;
import mx.events.EffectEvent;
import mx.logging.ILogger;
import mx.logging.Log;
import mx.rpc.Responder;
import spark.effects.AnimateTransitionShader;
import spark.effects.CrossFade;

/**
 *  The BitmapGallery component loads, displays and transitions between
 *  bitmaps.
 *
 *  @author gabriel montagné láscaris-comneno gabriel@rojored.com
 */
public class BitmapGallery extends UIComponent
{

    //--------------------------------------------------------------------------
    //
    //  Class constants
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    private static const LOG:ILogger =
        Log.getLogger(getQualifiedClassName(BitmapGallery).replace(/\W+/g,
            "."));

    //--------------------------------------------------------------------------
    //
    //  Constructor
    //
    //--------------------------------------------------------------------------

    /**
     *  Constructor
     */
    public function BitmapGallery()
    {
        super();
    }

    //--------------------------------------------------------------------------
    //
    //  Variables
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    private var animateTransitionShaderEnded:Boolean;

    /**
     *  @private
     */
    private var bitmap:Bitmap;

    /**
     *  @private
     */
    private var bitmapData:BitmapData;

    /**
     *  @private
     */
    private var bitmapDataChanged:Boolean;

    /**
     *  @private
     */
    private var bitmapAssetDownloader:BitmapAssetDownloader;

    /**
     *  @private
     */
    private var previousBitmapData:BitmapData;

    /**
     *  @private
     */
    private var previousTargetArea:Rectangle;

    /**
     *  @private
     */
    private var isAnimating:Boolean;

    //--------------------------------------------------------------------------
    //
    //  Properties
    //
    //--------------------------------------------------------------------------

    //--------------------------------------
    //  animateTransitionShader
    //--------------------------------------

    /**
     *  Effect that will transition between bitmaps using a custom pixel
     *  shader.
     */
    public var animateTransitionShader:AnimateTransitionShader =
        new CrossFade();

    //--------------------------------------
    //  debug
    //--------------------------------------

    /**
     *  If <code>true</code> the generated bitmaps won't be transparent but
     *  will have a random solid color.
     */
    public var debug:Boolean;

    //--------------------------------------
    //  maintainAspectRatio
    //--------------------------------------

    /**
     *  A flag that indicates whether to maintain the aspect ratio of the
     *  loaded content.
     */
    public var maintainAspectRatio:Boolean = true;

    //--------------------------------------
    //  source
    //--------------------------------------

    /**
     *  @private
     *  Storage for the source property
     */
    private var _source:String;

    /**
     *  @private
     */
    private var sourceChanged:Boolean;

    [Bindable("sourceChanged")]

    /**
     *  URL of the bitmap to display.
     */
    public function get source():String
    {
        return _source;
    }

    /**
     *  @private
     */
    public function set source(value:String):void
    {
        if (_source == value)
            return;

        _source = value;
        sourceChanged = true;
        invalidateProperties();
        dispatchEvent(new Event("sourceChanged"));
    }

    //--------------------------------------------------------------------------
    //
    //  Overriden methods: UIComponent
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    override protected function commitProperties():void
    {
        super.commitProperties();
        if (animateTransitionShaderEnded)
        {
            animateTransitionShaderEnded = false;
            unsubscribeToTransitionEvents(animateTransitionShader);
            if (previousBitmapData)
            {
                LOG.debug("Flushing previous bitmap data.");
                previousBitmapData.dispose();
                previousBitmapData = null;
            }
            if (animateTransitionShader && animateTransitionShader.bitmapFrom)
            {
                LOG.debug("Flushing tmp shader data.");
                animateTransitionShader.bitmapFrom.dispose();
                animateTransitionShader.bitmapFrom = null;
            }
            isAnimating = false;
        }

        if (isAnimating)
            return;

        if (sourceChanged)
        {
            LOG.debug("sourceChanged {0}", source);
            sourceChanged = false;
            if (bitmapAssetDownloader && bitmapAssetDownloader.isLoading)
                bitmapAssetDownloader.cancel();

            if (source)
                loadBitmap(source)
            else
                unloadBitmap();
        }
    }

    /**
     *  @private
     */
    override protected function createChildren():void
    {
        super.createChildren();
        if (!bitmap)
        {
            bitmap = new Bitmap();
            addChild(bitmap);
        }
    }

    /**
     *  @private
     */
    override protected function measure():void
    {
        super.measure();
        if (bitmapData)
        {
            measuredWidth =  bitmapData.width;
            measuredHeight = bitmapData.height;
            return;
        }
        measuredWidth =  0;
        measuredHeight = 0;
    }

    /**
     *  @private
     */
    override protected function updateDisplayList(unscaledWidth:Number,
                                                  unscaledHeight:Number):void
    {
        super.updateDisplayList(unscaledWidth, unscaledHeight);

        if (isAnimating)
            return;

        graphics.clear();

        if (bitmapDataChanged)
        {
            LOG.debug("bitmapDataChanged: ", bitmapData);
            bitmapDataChanged = false;
            commitBitmapData(unscaledWidth, unscaledHeight);
        }
    }

    //--------------------------------------------------------------------------
    //
    //  Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     *  Runs the bitmap data update logic for bitmap data changes.
     *  Called in the context of updateDisplayList.
     */
    private function commitBitmapData(width:Number, height:Number):void
    {
        var targetArea:Rectangle = new Rectangle(0, 0, width, height);
        if (!animateTransitionShader)
        {
            bitmap.bitmapData = bitmapData;
            if (bitmapData && maintainAspectRatio)
            {
                var endArea:Rectangle = MathUtil.fitRectangle(
                    new Rectangle(0, 0, bitmapData.width, bitmapData.height),
                    targetArea);
                bitmap.width = endArea.width;
                bitmap.height = endArea.height;
            }
            return;
        }

        if (!previousBitmapData && !bitmapData)
            return;

        previousTargetArea ||= targetArea.clone();

        var effectArea:Rectangle;
        var previousBitmapDataArea:Rectangle =
            previousBitmapData ?
                MathUtil.fitRectangle(
                    new Rectangle(
                        0,
                        0,
                        previousBitmapData.width,
                        previousBitmapData.height
                    ),
                    previousTargetArea
                )
                :   new Rectangle()
                ;
        if (maintainAspectRatio)
        {
            var bitmapDataArea:Rectangle =
                bitmapData ?
                MathUtil.fitRectangle(
                        new Rectangle(
                            0,
                            0,
                            bitmapData.width,
                            bitmapData.height
                        ),
                        targetArea
                )
                : new Rectangle()
                ;
            effectArea = previousBitmapDataArea.union(bitmapDataArea);
        }
        else
        {
            effectArea = previousBitmapDataArea.union(targetArea);
        }

        LOG.debug("fit bitmapFrom {0} => {1}", previousTargetArea, effectArea);
        var bitmapFrom:BitmapData =
            fitBitmapDataToTargetArea(
                previousBitmapData,
                previousTargetArea,
                effectArea,
                maintainAspectRatio
            );

        LOG.debug("fit bitmapTo {0} => {1}", targetArea, effectArea);
        var bitmapTo:BitmapData =
            fitBitmapDataToTargetArea(
                bitmapData,
                targetArea,
                effectArea,
                maintainAspectRatio
            );

        var bitmapArea:Rectangle = maintainAspectRatio && bitmapData ?
            MathUtil.fitRectangle(
                new Rectangle(0, 0, bitmapData.width, bitmapData.height),
                targetArea
            )
            : targetArea
            ;

        LOG.debug("bitmapArea {0}", bitmapArea);

        bitmap.bitmapData = bitmapTo;
        animateTransitionShader.target = bitmap;
        animateTransitionShader.bitmapFrom = bitmapFrom;
        animateTransitionShader.bitmapTo = bitmapTo;

        previousTargetArea = targetArea;
        subscribeToTransitionEvents(animateTransitionShader);
        isAnimating = true;
        animateTransitionShader.play();
    }

    /**
     *  @private
     *  Return a bitmap data with the target area and a scaled version of the
     *  bitmapData parameter if it was defined.
     */
    private function fitBitmapDataToTargetArea(bitmapData:BitmapData,
                                               targetArea:Rectangle,
                                               targetSize:Rectangle,
                                               maintainAspectRatio:Boolean):BitmapData
    {
        LOG.debug("fitBitmapDataToTargetArea: {0}", arguments.join("\n   : "));
        var bitmapDataArea:Rectangle = bitmapData ? new Rectangle(0, 0,
            bitmapData.width, bitmapData.height) : null;

        bitmapDataArea ||= targetArea;

        var resultBitmapData:BitmapData = new BitmapData(targetSize.width,
            targetSize.height, !debug, Math.random() * 0xFFFFFF);

        if (bitmapData)
        {
            var targetScaledArea:Rectangle = maintainAspectRatio ?
                MathUtil.fitRectangle(bitmapDataArea, targetArea) :
                targetArea;

            var scaleMatrix:Matrix = new Matrix(
                targetScaledArea.width / bitmapDataArea.width,
                0,
                0,
                targetScaledArea.height / bitmapDataArea.height
            );
            resultBitmapData.draw(bitmapData, scaleMatrix);
        }
        return resultBitmapData;
    }

    /**
     *  @private
     */
    private function subscribeToTransitionEvents(
                                        animateTransitionShader:IEventDispatcher):void
    {
        animateTransitionShader.addEventListener(
            EffectEvent.EFFECT_END,
            animateTransitionShader_effectEndHandler
        );
    }

    /**
     *  @private
     */
    private function unsubscribeToTransitionEvents(
                                        animateTransitionShader:IEventDispatcher):void
    {
        animateTransitionShader.removeEventListener(
            EffectEvent.EFFECT_END,
            animateTransitionShader_effectEndHandler
        );
    }

    /**
     *  @private
     *  Loads a bitmap from a URL.
     */
    private function loadBitmap(url:String):void
    {
        LOG.debug("loadBitmap: {0}", url);
        if (!bitmapAssetDownloader)
            bitmapAssetDownloader = new BitmapAssetDownloader();

        bitmapAssetDownloader.loadAsset(
            url,
            new Responder(
                bitmapAssetDownloader_resultHandler,
                bitmapAssetDownloader_faultHandler
            )
        );
    }

    /**
     *  @private
     */
    private function setBitmapData(bitmapData:BitmapData):void
    {
        previousBitmapData = this.bitmapData;
        this.bitmapData = bitmapData;
        bitmapDataChanged = true;
        invalidateSize();
        invalidateDisplayList();
    }

    /**
     *  @private
     *  Unloads the currently displayed bitmap.
     */
    private function unloadBitmap():void
    {
        LOG.debug("unloadBitmap");
        setBitmapData(null);
    }

    //--------------------------------------------------------------------------
    //
    //  Event handlers
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    private function bitmapAssetDownloader_resultHandler(
                                            bitmapData:BitmapData):void
    {
        LOG.debug("bitmapAssetDownloader_resultHandler {0}", bitmapData);
        setBitmapData(bitmapData);
    }

    /**
     *  @private
     */
    private function bitmapAssetDownloader_faultHandler(info:Object):void
    {
        LOG.error("bitmapAssetDownloader_faultHandler {0}", info);
        setBitmapData(null);
    }

    /**
     *  @private
     */
    private function animateTransitionShader_effectEndHandler(
                                                    event:EffectEvent):void
    {
        animateTransitionShaderEnded = true;
        invalidateProperties();
    }
}
}
